package com.discovery.main;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Reader;
import java.io.StringReader;
import java.io.StringWriter;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Vector;

import javax.xml.XMLConstants;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Result;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;
import javax.xml.validation.Validator;

import org.dom4j.DocumentException;
import org.dom4j.io.SAXReader;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

/**
 * XMLUtil encapsulates general XML related utility methods.
 */
public class XMLUtil
{
//   private static final Logger log = LoggerFactory.getLogger(XMLUtil.class);

   /**
    * Finds all the children node whose tag names are in tags ArrayList. For
    * example to get <one> and <two> in Vector of Node from the following XML
    * node : <parent> <one> something </one> <two> something </two> <three>
    * something </three> </parent> you pass <parent> node along with this list
    * {"one","two"}
    *
    * @param parentNode : parentNode to lookup into for tags
    * @param tags : list of tags to be found among parentNode children
    * @return map of <NodeName, Node> that is found based on tags given in tags
    *         arrayList
    */
   public static HashMap<String, Node> getChildrenByTagNames(Node parentNode,
                                                             ArrayList<String> tags)
   {
      HashMap<String, Node> resultList = new HashMap<String, Node>();
      String tagFromList = null;
      Node foundNode = null;
      if (tags.size() > 0 || parentNode != null) {
         for (int tagNo = 0; tagNo < tags.size(); tagNo++) {
            tagFromList = tags.get(tagNo).toString();
            foundNode = XMLUtil.getInnerNodeByTagName(parentNode, tagFromList);
            if (!resultList.containsKey(tagFromList)) {
               resultList.put(tagFromList, foundNode);
            }
         }
      }
      return resultList.size() == 0 ? null : resultList;
   }

   /**
    * Puts the node attributes (if it has any) into HashMap<String,String>
    *
    * @param node : XML node to look for its attributes
    * @return Hashmap<String,String> of the node attributes
    */
   public static HashMap<String, String> getAttributes(Node node)
   {
      String attrName = null;
      String attrValue = null;
      HashMap<String, String> attributesMap = new HashMap<String, String>();
      NamedNodeMap attributes = node.getAttributes();
      for (int attrIndex = 0; attributes != null
               && attrIndex < attributes.getLength(); attrIndex++) {
         attrName = attributes.item(attrIndex).getNodeName();
         attrValue = attributes.item(attrIndex).getNodeValue();
         attributesMap.put(attrName, attrValue);
      }
      return attributesMap.size() == 0 ? null : attributesMap;
   }

   /**
    * Puts the node attributes (if it has any) into HashMap<String,String>
    * Retrieves only those attributes that are in attr ArrayList Ignores other
    * attributes values that are in node
    *
    * @param node : XML node to look for its attributes
    * @param attrNames : attributes to fetch from node
    * @return Hashmap<String,String> of the node attributes
    */
   public static HashMap<String, String> getAttributesByName(Node node,
                                                             ArrayList<String> attrNames)
   {
      String attrName = null;
      String attrValue = null;
      HashMap<String, String> attributesMap = new HashMap<String, String>();
      NamedNodeMap attributes = node.getAttributes();
      for (int attrIndex = 0; attrIndex < attributes.getLength(); attrIndex++) {
         attrName = attributes.item(attrIndex).getNodeName();
         attrValue = attributes.item(attrIndex).getNodeValue();
         if (attrNames.contains(attrName)) {
            attributesMap.put(attrName, attrValue);
         }
      }
      return attributesMap.size() == 0 ? null : attributesMap;
   }

   /**
    * Goes through inner nodes of parentNode and find the first node with
    * specified tagName
    *
    * @param parentNode : Node of the parent
    * @param tagName : tag name
    * @return Node : inner node of the parent that has the specific tag name
    */
   public static Node getInnerNodeByTagName(Node parentNode,
                                            String tagName)
   {
      Node node = null;
      Node child = null;
      if (parentNode != null) {
         NodeList children = parentNode.getChildNodes();
         for (int i = 0; i < children.getLength(); i++) {
            child = children.item(i);
            if (child.getNodeType() == Node.ELEMENT_NODE
                     && child.getNodeName().equalsIgnoreCase(tagName)) {
               node = child;
               break;
            }
         }
      }
      return node;
   }

   /**
    * Extracts the inner text value from a xml node accept : <node>value</node>
    * and return value string
    *
    * @param node - xml node to extract inner text from
    * @return innerText if its not empty otherwise it returns null
    */
   public static String getNodeTextValue(Node node)
   {
      String innerText = null;
      if (node != null) {
         Node textNode = node.getFirstChild();
         if (textNode != null && textNode.getNodeType() == Node.TEXT_NODE) {
            innerText = textNode.getNodeValue();
            innerText.trim();
            if (innerText.length() == 0) {
               innerText = null;
            }
         }
      }
      return innerText;
   }

   /**
    * Finds all the children node and put it in HashMap<String,Node> where it
    * represents tagName and Node For example to get all the children of tag
    * <parent> from the following xml node : <parent> <one> something </one>
    * <two> something </two> <three> something </three> </parent> you pass
    * <parent> node and it returns {{("one",<one>) ("two",<two>)
    * ("three",<three>)}
    *
    * @param parentNode : parentNode to lookup into for tags
    * @return HashMap of <NodeName,Node> that is found based on tags given in
    *         tags arrayList
    */
   public static HashMap<String, Node> getChildrenByTagNames(Node parentNode)
   {
      HashMap<String, Node> resultList = new HashMap<String, Node>();
      Node child = null;
      if (parentNode != null) {
         NodeList children = parentNode.getChildNodes();
         for (int i = 0; i < children.getLength(); i++) {
            child = children.item(i);
            if (child.getNodeType() == Node.ELEMENT_NODE) {
               resultList.put(child.getNodeName(), child);
            }
         }
      }
      return resultList.size() == 0 ? null : resultList;
   }

   /**
    * Finds all the children node from the XML node, not considering the tag
    * name. For example if below is the input XML. <parent> <child> zero
    * </child> <child> one </child> <child> two </child> </parent> The result
    * List will have below three elements in a sequence. zero, one, two
    *
    * @param parentNode parentNode to lookup into for child nodes.
    * @return List of element nodes.
    */
   public static List<Node> getChildren(Node parentNode)
   {
      List<Node> resultList = new ArrayList<Node>();
      Node child = null;
      if (parentNode != null) {
         NodeList children = parentNode.getChildNodes();
         for (int i = 0; i < children.getLength(); i++) {
            child = children.item(i);
            if (child.getNodeType() == Node.ELEMENT_NODE) {
               resultList.add(child);
            }
         }
      }
      return resultList.size() == 0 ? null : resultList;
   }

   /**
    * Creates the parser instance based on dom parser factory Validates the xml
    * file against the XSD schema Return the documentElement of the xml document
    * if validations succeeded
    *
    * @param xmlFile - XML file to be parsed
    * @param xmlSchema - XSD that XML file should be validated against
    * @return documentElement of the XML file
    */
   public static Document getXmlDocumentElement(String xmlFile,
                                                String xmlSchema)
      throws Exception
   {
      DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
      factory.setCoalescing(true);
      factory.setNamespaceAware(false);
      factory.setIgnoringElementContentWhitespace(true);
      factory.setValidating(true);
      factory.setAttribute(
               "http://java.sun.com/xml/jaxp/properties/schemaLanguage",
               "http://www.w3.org/2001/XMLSchema");
      factory.setAttribute(
               "http://java.sun.com/xml/jaxp/properties/schemaSource",
               XMLUtil.class.getResource(xmlSchema).getFile());
      DocumentBuilder builder = factory.newDocumentBuilder();
      builder.setErrorHandler(new org.xml.sax.helpers.DefaultHandler());

      //Parse the document from the classpath.
      URL xmlFileUri = XMLUtil.class.getResource(xmlFile);
      if (null == xmlFileUri) {
         System.out.println("Unable to find file on classpath: " + xmlFile);
         return null;
      }
      return builder.parse(xmlFileUri.getFile());
   }

   /**
    * Creates the parser instance based on DOM parser factory, using the
    * contents of a XML file. Validation is not done. Return the documentElement
    * of the XML document
    *
    * @param xmlFile - XML file to be parsed
    * @return documentElement of the XML file
    */
   public static Document getXmlDocumentElement(String xmlFileContents)
      throws Exception
   {
      DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
      Reader reader = new StringReader(xmlFileContents);
      InputSource inputSource = new InputSource(reader);
      factory.setCoalescing(true);
      factory.setNamespaceAware(false);
      factory.setIgnoringElementContentWhitespace(true);
      factory.setValidating(false);
      DocumentBuilder builder = factory.newDocumentBuilder();
      return builder.parse(inputSource);
   }

   /**
    * Gets the descendant elements which have an attribute with the specified
    * name and value
    *
    * @param parentNode - Node whose children are to be searched
    * @param attrId - attribute name
    * @param attrVal - attribute value
    * @return List of all children nodes
    * @throws Exception
    */
   public static List<Element> getElements(Node parentNode,
                                           String attrId,
                                           String attrVal)
      throws Exception
   {
      NodeList nodeList = parentNode.getChildNodes();
      List<Element> elements = new ArrayList<Element>();
      for (int i = 0; i < nodeList.getLength(); i++) {
         Node child = nodeList.item(i);
         if (child instanceof Element) {
            Element element = (Element) child;
            if (attrVal.equalsIgnoreCase(element.getAttribute(attrId))) {
               elements.add(element);
            }
            if (element.hasChildNodes()) {
               // recursive call to search for child's children
               List<Element> children = getElements(element, attrId, attrVal);
               if (children != null) {
                  elements.addAll(children);
               }
            }
         }
      }
      return elements.isEmpty() ? null : elements;
   }

   /**
    * Gets the descendant elements which have an attribute with the specified
    * name and value
    *
    * @param xmlInput - XML data as string
    * @param attrId - attribute name
    * @param attrVal - attribute value
    * @return List of all children nodes
    * @throws Exception
    */
   public static List<Element> getElements(String xmlInput,
                                           String attrId,
                                           String attrVal)
      throws Exception
   {
      Document doc = XMLUtil.getXmlDocumentElement(xmlInput);
      return XMLUtil.getElements(doc, attrId, attrVal);
   }

   /**
    * Validates an XML file with its schema.
    *
    * @param xmlFileContents - The XML file contents
    * @param xmlSchemaFilePath - XSD file path
    * @throws SAXException
    * @throws Exception
    */
   public static void validateXML(String xmlFileContents,
                                  String xmlSchemaFilePath)
      throws Exception
   {
      Document document = XMLUtil.getXmlDocumentElement(xmlFileContents);
      validateDocument(document, new FileInputStream(xmlSchemaFilePath));
   }

   /**
    * Validates an XML file with its schema.
    *
    * @param xmlFileStream - the inputStream of the xml content
    * @param xsdFileStream - the inputStream of the xsd content
    * @throws Exception
    */
   public static void validateXML(InputStream xmlFileStream,
                                  InputStream xsdFileStream)
      throws Exception
   {
      DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
      documentBuilderFactory.setNamespaceAware(true);
      documentBuilderFactory.setIgnoringElementContentWhitespace(true);
      documentBuilderFactory.setValidating(false);
      DocumentBuilder parser = documentBuilderFactory.newDocumentBuilder();
      Document document = parser.parse(xmlFileStream);
      validateDocument(document, xsdFileStream);
   }

   /**
    * Validates the given document with its schema.
    *
    * @param document - The XML file contents
    * @param schemaFileStream - the inputStream of the schema content
    * @throws Exception
    */
   public static void validateDocument(Document document,
                                       InputStream schemaFileStream)
      throws Exception
   {
      // create a SchemaFactory capable of understanding WXS schemas
      SchemaFactory factory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);

      // load a WXS schema, represented by a Schema instance
      Source schemaFile = new StreamSource(schemaFileStream);
      Schema schema = factory.newSchema(schemaFile);

      // create a Validator instance, which can be used to validate an
      // instance document
      Validator validator = schema.newValidator();

      // validate the DOM tree
      validator.validate(new DOMSource(document));
   }

   /**
    * Get child nodes of the first element that has the specified tag name and
    * also contains the specified attribute id and value
    *
    * @param xmlFile XML file contents
    * @param elementName name of the required element
    * @param attrId Id of an attribute in element
    * @param attrVal attribute value for the given attribute
    * @return NodeList childNodes if the given element name with the given
    *         attributeId and attributeValue exists, otherwise null
    * @throws Exception
    */
   public static NodeList getChildNodes(String xmlFile,
                                        String elementName,
                                        String attrId,
                                        String attrVal)
      throws Exception
   {
      NodeList childNodes = null;
      Document xmlDoc = getXmlDocumentElement(xmlFile);
      NodeList elements = xmlDoc.getElementsByTagName(elementName);
      for (int i = 0; i < elements.getLength(); i++) {
         Element element = (Element) elements.item(i);
         if (element.hasAttribute(attrId)) {
            if (element.getAttribute(attrId).equals(attrVal)) {
               childNodes = element.getChildNodes();
               break;
            }
         }
      }
      return childNodes;
   }

   /*
    * Modifies the value of first node of specified node name in XML file with
    * the new value and saves back the XML file with the modified value.
    *
    * @param xmlPath Path of XML file.
    *
    * @param nodeName Node name to be modified
    *
    * @param newValue New value of the node
    *
    * @throws Exception
    */
   public static void updateXmlNodeValue(String xmlPath,
                                         String nodeName,
                                         String newValue)
      throws Exception
   {
      /*
       * Reads XML file from the specified path.
       */
      BufferedReader reader = new BufferedReader(new FileReader(xmlPath));
      StringBuffer buffer = new StringBuffer();

      while (true) {
         int i = reader.read();
         if (i == -1)
            break;
         buffer.append((char) i);
      }
      String xmlString = buffer.toString();
      Document xmlDoc = getXmlDocumentElement(xmlString);
      NodeList list = xmlDoc.getElementsByTagName(nodeName);
      Node node = list.item(0);
      Node toChangeNode = node.getFirstChild();
      /*
       * Set the new value.
       */
      toChangeNode.setNodeValue(newValue);
      /*
       * Save XML doc to the filesystem path.
       */
      saveDocument(xmlDoc, xmlPath);
   }

   /**
    * Saves Document instance to XML file to the specified filePath.
    *
    * @param xmlDoc - Document instance
    * @param destPath - dest path where the XML file is to be saved.
    * @throws Exception
    */
   public static void saveDocument(Document xmlDoc,
                                   String destPath)
      throws Exception
   {
      Source xmlSource = new DOMSource(xmlDoc);
      OutputStream os = new FileOutputStream(destPath);
      Result result = new StreamResult(os);
      TransformerFactory transFactory = TransformerFactory.newInstance();
      Transformer transformer = transFactory.newTransformer();
      /*
       * output to file
       */
      transformer.transform(xmlSource, result);
   }

   /**
    * Returns the text value of a node where a sibling tag name and value is
    * known.
    * <p>
    * For example, in the XML snippet below, if you pass in "cat" as the
    * parentTagName, "name" as the childTagName, and "MrBiggles" as the
    * childTagValue, and specify "color" as the siblingTagName, the method would
    * return "Grey".
    * <p>
    * Example XML:
    *
    * <pre>
    * &lt;cat&gt;
    *    &lt;name&gt;MrBiggles&lt;/name&gt;
    *    &lt;color&gt;Grey&lt;/color&gt;
    * &lt;/cat&gt;
    * &lt;cat&gt;
    *    &lt;name&gt;Boris&lt;/name&gt;
    *    &lt;color&gt;Black&lt;/color&gt;
    * &lt;/cat&gt;
    * </pre>
    *
    * @param xmlDocAsString The XML document as a String
    * @param parentTagName The tag name of the parent elements that should
    *           contain the sibling nodes
    * @param childTagName The tag name of the known child element
    * @param childTagValue The known text value of the child element
    * @param siblingTagName The tag name of the sibling node
    * @return String The text value of the sibling node
    * @throws Exception
    */
   public static String findSiblingNodeValue(String xmlDocAsString,
                                             String parentTagName,
                                             String childTagName,
                                             String childTagValue,
                                             String siblingTagName)
      throws Exception
   {
      String siblingNodeValue = null;
      Document xmlDoc = XMLUtil.getXmlDocumentElement(xmlDocAsString);
      NodeList parentNodes = xmlDoc.getElementsByTagName(parentTagName);
      if (parentNodes != null) {
         for (int i = 0; i < parentNodes.getLength(); i++) {
            Node parentNode = parentNodes.item(i);
            HashMap<String, Node> childNodes = XMLUtil.getChildrenByTagNames(parentNode);
            Node childNode = childNodes.get(childTagName);
            String tmpChildNodeVal = XMLUtil.getNodeTextValue(childNode);
            if (childTagValue.equals(tmpChildNodeVal)) {
               System.out.println("Found node: " + childTagName
                        + " with value: " + childTagValue);
               Node siblingNode = childNodes.get(siblingTagName);
               if (siblingNode != null) {
                  siblingNodeValue = XMLUtil.getNodeTextValue(siblingNode);
                  System.out.println("Found the sibling node: " + siblingTagName
                           + " = " + siblingNodeValue);
                  break;
               } else {
                  System.out.println("No sibling node named: "
                           + siblingTagName + " was found.");
               }
            }
         }
      } else {
         System.out.println("There were no elements with tag name: "
                  + parentTagName);
      }
      return siblingNodeValue;
   }

   /**
    * Returns the values of all tags under the root element with the specified
    * tag name.
    *
    * @param xml XML String to search
    * @param childTagName The name of the tag to get the value of
    * @return List of Strings - the text of the desired metadata.
    * @throws Exception
    */
   public static List<String> getNodeTextValuesByTagName(String xml,
                                                         String childTagName)
      throws Exception
   {
      List<String> values = new ArrayList<String>();
      Document xmldoc = XMLUtil.getXmlDocumentElement(xml);
      Node parentNode = xmldoc.getDocumentElement();
      if (parentNode != null) {
         NodeList children = parentNode.getChildNodes();
         Node child = null;
         for (int i = 0; i < children.getLength(); i++) {
            child = children.item(i);
            if (child.getNodeType() == Node.ELEMENT_NODE
                     && child.getNodeName().equalsIgnoreCase(childTagName)) {
               String nodeVal = XMLUtil.getNodeTextValue(child);
               values.add(nodeVal);
            }
         }
      }

      return values;
   }

   /**
    * Returns the text value of the first tag under the root element with the
    * specified tag name.
    *
    * @param xml XML String to search
    * @param childTagName The name of the tag to get the value of
    * @return List of Strings - the text of the desired metadata.
    * @throws Exception
    */
   public static String getNodeTextValueByTagName(String xml,
                                                  String childTagName)
      throws Exception
   {
      Document xmldoc = XMLUtil.getXmlDocumentElement(xml);
      Node parentNode = xmldoc.getDocumentElement();
      Node metadataNode = XMLUtil.getInnerNodeByTagName(parentNode,
               childTagName);
      String nodeVal = XMLUtil.getNodeTextValue(metadataNode);
      return nodeVal;
   }

//   /**
//    * Compare one XML file contents with another reference XML file.
//    *
//    * @param xmlFileContents contents of the file as String
//    * @param refFileContents contents of the reference XML file
//    * @return 0 if equal 1 if not equal 2 if an error occurred
//    * @throws Exception
//    */
//   public static int compareWithReferenceFile(String xmlFileContents,
//                                              String refFileContents)
//      throws Exception
//   {
//      // return 0;
//      return ExamXML.compareXMLEntities(refFileContents, xmlFileContents);
//   }

//   /**
//    * Get the difference between two XML files as a String.
//    *
//    * @param xmlFileContents contents of the XML file
//    * @param refFileContents contents of the other XML file
//    * @return Difference between the files as a String
//    */
//   public static String getXMLDiffAsString(String xmlFileContents,
//                                           String refFileContents)
//   {
//      return ExamXML.compareXMLString(refFileContents, xmlFileContents);
//      // return null;
//   }

   public static Vector<String> getValueOnTag(Document doc,
            String elementName)
   {
      Vector<String> elementNames = new Vector<String>();
      NodeList list = doc.getElementsByTagName(elementName);
      System.out.println("XML Elements: ");
      for (int i = 0; i < list.getLength(); i++) {
         Element element = (Element) list.item(i);
         elementNames.add(element.getAttribute("name"));
         System.out.println("Element Name : " + element.getLocalName()
         + " Value :" + element.getAttribute("name"));
      }
      return elementNames;
   }

   public static Document getXMLDocument(String xmlFileName)
   {
      Document doc = null;
      try {
         File file = new File(xmlFileName);
         DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
         DocumentBuilder db = dbf.newDocumentBuilder();
         doc = db.parse(file);
      } catch (Exception e) {
         System.out.println("Exception @ InventoryManager.getXMLDocument "
         + e.getMessage());
         e.printStackTrace();
      }
      return doc;
   }

   /**
    * Given a W3C Node, gives the XML string representation
    * @param node a W3C XML Node
    * @return node as string
    */
   public static String getNodeAsString(Node node)
   {
      try {
         Source source = new DOMSource(node);
         StringWriter stringWriter = new StringWriter();
         Result result = new StreamResult(stringWriter);
         TransformerFactory factory = TransformerFactory.newInstance();
         Transformer transformer = factory.newTransformer();
         transformer.setOutputProperty(OutputKeys.INDENT, "yes");
         transformer.transform(source, result);
         return stringWriter.getBuffer().toString();
     } catch (TransformerConfigurationException e) {
         System.out.println("Caught Exception "+ e);
     } catch (TransformerException e) {
        System.out.println("Caught Exception "+ e);
     }
     return null;
   }

   /**
    * Reads the xml file from the specified path and returns a dom4j document
    *
    * e.g : adapter/folder/example.xml
    *
    * @return created document
    * @throws org.dom4j.DocumentException -
    */
   public static org.dom4j.Document
   loadXml(String xmlPath)
           throws DocumentException
   {
      InputStream inputStream =
         ClassLoader.getSystemClassLoader().getResourceAsStream(
            xmlPath);
      SAXReader reader = new SAXReader();
      org.dom4j.Document document = reader.read(inputStream);
      if (document == null) {
         System.out.println(
            "unable to load datastore-create.xml file");
      }
      return document;
   }



}
